package it.tukano.treditor.modules;

import it.tukano.app.AbstractAppModule;
import it.tukano.app.AppModule;
import it.tukano.check.Checks;
import it.tukano.check.Todo;
import it.tukano.fun.Function1;
import it.tukano.fun.OptionalValue;
import it.tukano.fun.VFunction1;
import it.tukano.fun.VFunction2;
import it.tukano.fun.VFunction3;
import it.tukano.io.FileExt;
import it.tukano.log.Log;
import it.tukano.swingdeco.containers.JPanelExt;
import it.tukano.swingdeco.draganddrop.DndUtilities;
import it.tukano.swingdeco.tree.JTreeExt;
import it.tukano.treditor.engine.scene.DirectionalLightElement;
import it.tukano.treditor.engine.scene.ModelElement;
import it.tukano.treditor.engine.scene.NodeElement;
import it.tukano.treditor.engine.scene.SceneElement;
import it.tukano.treditor.modules.datamodel.DataModelEvent;
import it.tukano.treditor.modules.datamodel.DataModelListener;
import it.tukano.treditor.resources.Icons;
import it.tukano.treditor.resources.Labels;
import it.tukano.treditor.resources.Labels.LabelKey;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Point;
import java.io.File;
import java.net.URL;
import java.util.Arrays;
import java.util.Map;
import javax.swing.JLabel;
import javax.swing.tree.DefaultMutableTreeNode;

/**
 * This module displayes the scene graph structure in a tree
 * @author pgi
 */
public class SceneGraphViewer extends AbstractAppModule implements DataModelListener {
    
    /* The tree */
    private final JTreeExt tree = new JTreeExt()
            .setSimpleRenderer(NodeElement.class, new VFunction2<NodeElement, JLabel>() {
                private final Dimension ICON_SIZE = new Dimension(20, 20);

                @Override
                public void doApply(NodeElement a, JLabel b) {
                    b.setIcon(Icons.get(Icons.NODE_ICON, ICON_SIZE));
                    b.setText(a.getName());
                }
            })
            .onSelectionChange(new VFunction1<DefaultMutableTreeNode>() {

                @Override
                protected void doApply(DefaultMutableTreeNode p) {
                    SceneGraphViewer.this.notifySelectedTreeNodeHasChanged(p);
                }
            });
    
    /* The tree's container */
    private final JPanelExt treeContainer = new JPanelExt(new BorderLayout())
            .append(tree.getScroller(), BorderLayout.CENTER)
            .withPreferredSize(250, 200)
            .withName(Labels.getInstance().getLabelValue(LabelKey.SCENE_GRAPH_VIEWER_TITLE));

    /**
     * Instance initializer
     */
    public SceneGraphViewer() {
        DndUtilities.installDropHandler(tree, Arrays.<Class<?>>asList(File.class), new VFunction3<Component, Point, Map<Class<?>, Object>>() {

            @Override protected void doApply(Component c, Point dropLocation, Map<Class<?>, Object> droppedElements) {
                final OptionalValue<DefaultMutableTreeNode> targetNode = tree.getNodeAt(dropLocation);
                if(targetNode.isEmpty()) return;
                final DefaultMutableTreeNode node = targetNode.get();
                final File file = (File) droppedElements.get(File.class);
                if(file != null) {
                    SceneGraphViewer.this.addModelToSceneTree(file, node);
                }
            }
        });
    }

    /**
     * Shows the viewer in the main frame
     * @param context the app context
     */
    @Override
    public void startModule(AppModule context) {
        MainFrame mainFrame = find(MainFrame.class);
        DataModel dataModel = find(DataModel.class);
        mainFrame.install(treeContainer, BorderLayout.WEST, true);
        dataModel.addListener(this);
    }

    /**
     * Handles events notified by the data model if the source is not this
     * @param e the event to handle
     */
    public void dataModelEventPerformed(DataModelEvent e) {
        if(e.getEventSource() == this) return;//self notify
        
        switch(e.getType()) {
            case SCENE_ROOT_SET:
                doReplaceSceneRootTreeElement(e);
                break;
            case NODE_ADD_EMPTY_NODE:
                doAddEmptyNodeToNode(e);
                break;
            case NODE_ADD_DIRECTIONAL_LIGHT:
                doAddDirectionalLightToNode(e);
                break;
            case SELECTED_ELEMENT_SET:
                setSelectedElement(e);
                break;
        }
    }

    /**
     * Replaces the tree element that displayes the scene root
     * @param e the event to handle
     */
    private void doReplaceSceneRootTreeElement(DataModelEvent e) {
        DefaultMutableTreeNode treeRoot = tree.getRoot();
        treeRoot.removeAllChildren();
        NodeElement newRoot = e.getArg(0, NodeElement.class);
        generateBranch(treeRoot, newRoot);
        tree.update(treeRoot);
        tree.expand(treeRoot);
    }

    /**
     * Generate the tree branch that starts with the given element
     * @param branchRoot the new branch root
     * @param element the element to transform into a tree branch. The node
     * built for this element will be inserted into the branchRoot. The tree model
     * is not updated so we need to call tree.update(branchRoot) after this method
     * to show the changes in the tree
     */
    private void generateBranch(DefaultMutableTreeNode branchRoot, SceneElement element) {
        DefaultMutableTreeNode subBranchRoot = tree.insert(element, branchRoot);
        switch(element.getType()) {
            case NODE:
                NodeElement node = (NodeElement) element;
                for(int i = 0; i < node.getChildrenCount(); i++) {
                    generateBranch(subBranchRoot, node.getChild(i));
                }
                break;
        }
    }

    /**
     * Called to add a model to the scene tree.
     * @param modelFile the model file to add
     * @param targetTreeNode the tree node where to add the model
     */
    public void addModelToSceneTree(File modelFile, DefaultMutableTreeNode targetTreeNode) {
        Checks.NotNull(modelFile);
        Checks.NotNull(targetTreeNode);
        
        final EngineManager engineManager = find(EngineManager.class);
        final FileExt modelFileExt = new FileExt(modelFile);
        final URL modelUrl = modelFileExt.getURL();
        final Object userObject = targetTreeNode.getUserObject();
        
        Checks.NotNull(engineManager);
        Checks.InstanceOf(userObject, SceneElement.class);
        
        final SceneElement targetSceneElement = (SceneElement) userObject;
        
        final boolean targetElementIsNodeElement = targetSceneElement.getType() == SceneElement.Type.NODE;
        final boolean modelFormatSupported = engineManager.isModelFormatSupported(modelUrl);
        
        if(targetElementIsNodeElement && modelFormatSupported) {
            final NodeElement targetElement = (NodeElement) targetSceneElement;
            final ModelElement childElement = new ModelElement();
            childElement.setModelFileAddress(modelUrl);
            targetElement.addChild(childElement);
            tree.insert(childElement, targetTreeNode);
            tree.expand(targetTreeNode);
            final DataModel dataModel = find(DataModel.class);
            dataModel.notifyModelElementAddToNode(this, childElement, targetElement);
        } else {
            Todo.implementThis("Model format supported: " + modelFormatSupported);
        }
    }

    /**
     * Returns the component that can be used to display popup menus
     * @return the popup menu parent component
     */
    public Component getSceneGraphPopupHook() {
        return tree;
    }

    public SceneElement getSceneElementAtPopupHookLocation(Point atLocation) {
        SceneElement result = null;
        OptionalValue<DefaultMutableTreeNode> nodeAt = tree.getNodeAt(atLocation);
        if(nodeAt.isDefined()) {
            DefaultMutableTreeNode node = nodeAt.get();
            Object userObject = node.getUserObject();
            if(userObject instanceof SceneElement) {
                result = (SceneElement) userObject;
            }
        }
        return result;
    }

    private void doAddEmptyNodeToNode(DataModelEvent e) {
        NodeElement parent = e.getArg(0, NodeElement.class);
        NodeElement emptyChild = e.getArg(1, NodeElement.class);
        DefaultMutableTreeNode parentNode = tree.insertAndGetParent(emptyChild, parent);
        tree.expand(parentNode);
    }

    private void doAddDirectionalLightToNode(DataModelEvent e) {
        NodeElement parent = e.getArg(0, NodeElement.class);
        DirectionalLightElement light = e.getArg(1, DirectionalLightElement.class);
        DefaultMutableTreeNode parentNode = tree.insertAndGetParent(light, parent);
        tree.expand(parentNode);
    }

    private void notifySelectedTreeNodeHasChanged(DefaultMutableTreeNode selectedNode) {
        final DataModel dataModel = find(DataModel.class);
        final boolean hasSelection = selectedNode != null;
        if(hasSelection) {
            final Object userObject = selectedNode.getUserObject();
            Checks.InstanceOf(userObject, SceneElement.class);
            final SceneElement sceneElement = (SceneElement) userObject;
            dataModel.setSelectedElement(this, sceneElement);
        } else {
            dataModel.setSelectedElement(this, null);
        }
    }

    public void removeElement(SceneElement element) {
        final OptionalValue<DefaultMutableTreeNode> node = tree.findFirstNodeByUserObject(element);
        Checks.True(node.isDefined());
        final DefaultMutableTreeNode treeNode = node.get();
        Checks.True(treeNode.getUserObject() == element);
        tree.remove(treeNode);
        final SceneElement parentElement = element.getParent();
        switch(parentElement.getType()) {
            case NODE:
                final NodeElement parentNode = (NodeElement) parentElement;
                parentNode.removeChild(element);
                find(DataModel.class).notifyElementRemovedFromNode(this, element, parentNode);
                break;
            default:
                Todo.implementThis();
        }
    }

    private void setSelectedElement(DataModelEvent e) {
        final SceneElement selectedElement = e.getArgOrElse(0, SceneElement.class, null);
        if(selectedElement == null) {
            tree.clearSelection();
        } else {
            OptionalValue<DefaultMutableTreeNode> findFirstNodeByUserObject = tree.findFirstNodeByUserObject(selectedElement);
            if(findFirstNodeByUserObject.isDefined()) tree.setSelectedNode(findFirstNodeByUserObject.get(), true);
        }
    }
}
